#!/usr/bin/python3
# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t -*-

import os
import re
import abc
import enum
import json
import signal
import logging
import functools
import subprocess
import configparser
from fm_util import Util
from fm_param import FmConst
from fm_handy import FmScreen, FmWayFire
from gi.repository import GLib


class UsrSessionHelperBase(abc.ABC):

    def __init__(self, parent, session_id):
        self._p = parent
        self._sessionId = session_id

    @abc.abstractmethod
    def start(self, machine):
        pass

    @abc.abstractmethod
    def exit(self):
        pass


class WayFireInstance:

    def __init__(self, session_id, config_filepath, machine, imf_type, sound_server_type, headless):
        # wayfire config ini
        FmWayFire.check_template_config_file(bAutoFix=True)
        if True:
            cfg = configparser.ConfigParser(interpolation=None)
            cfg.read(FmWayFire.get_template_config_filepath())

            # modify output
            for d in Util.hwcfgGetScreenDevices(machine):
                outputName = re.fullmatch('card[0-9]+-(.*)', d.get_drm_name()).group(1)     # why wayfire don't use the "cardX" prefix?
                outputName = "output:%s" % (outputName)
                if not cfg.has_section(outputName):
                    cfg.add_section(outputName)
                if True:
                    srcResX = int(d.get_resolution().split("x")[0])
                    dstResX = FmScreen.get_best_resolution_x(machine.get_type(), d.get_aspect_ratio(), d.get_screen_diagonal())
                    if dstResX is not None:
                        if dstResX < srcResX:
                            cfg.set(outputName, "scale", srcResX / dstResX)
                        else:
                            # only zoom-in, don't zoom-out
                            pass
                    else:
                        # dstResX is None means don't scale
                        pass
                # FIXME
                # cfg.set("output", "mode", "auto")
                # cfg.set("output", "position", "auto")
                # cfg.set("output", "transform", "normal")

            # modify autostart
            if not cfg.has_section("autostart"):
                cfg.add_section("autostart")
            cfg.set("autostart", "getenv", "%s %s" % (os.path.join(FmConst.libexecDir, "retrieve-compositor-environment-variables.py"), session_id))

            # modify screen shot key binding
            if True:
                if not cfg.has_section("command"):
                    cfg.add_section("command")
                cfg.set("command", "binding_screenshot", "KEY_PRINT")
                cfg.set("command", "command_screenshot", "grim - | wl-copy")
                cfg.set("command", "binding_screenshot_interactive", "<shift> KEY_PRINT")
                cfg.set("command", "command_screenshot_interactive", "grim -g `slurp` - | wl-copy")

            # modify imf key binding
            if imf_type == "fcitx":
                if not cfg.has_section("command"):
                    cfg.add_section("command")
                cfg.set("command", "binding_imf", "<control> space")      # FIXME: <Control>space should be read from imf cfg
                cfg.set("command", "command_imf", "fcitx5-remote -t")
            elif imf_type == "none":
                pass
            else:
                assert False

            # modify volume media key binding
            if sound_server_type == "pipewire":
                if not cfg.has_section("command"):
                    cfg.add_section("command")
                cfg.set("command", "repeatable_binding_volume_up", "KEY_VOLUMEUP")
                cfg.set("command", "command_volume_up", "wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+")
                cfg.set("command", "repeatable_binding_volume_down", "KEY_VOLUMEDOWN")
                cfg.set("command", "command_volume_down", "wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-")
                cfg.set("command", "binding_mute", "KEY_MUTE")
                cfg.set("command", "command_mute", "wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle")
            elif sound_server_type == "none":
                pass
            else:
                assert False

            # FIXME
            if True:
                if not cfg.has_section("command"):
                    cfg.add_section("command")
                cfg.set("command", "repeatable_binding_brightness_up", "KEY_BRIGHTNESSUP")
                cfg.set("command", "command_brightness_up", "brightnessctl set +5%")
                cfg.set("command", "repeatable_binding_brightness_down", "KEY_BRIGHTNESSDOWN")
                cfg.set("command", "command_brightness_down", "brightnessctl set 5%-")
            else:
                assert False

            # write
            with open(config_filepath, 'w') as f:
                cfg.write(f)

        # run compositor
        envDict = os.environ.copy()
        if headless:
            envDict.update({
                "WLR_BACKEND": "headless",
            })
        self._proc = subprocess.Popen(["wayfire", "-c", config_filepath], env=envDict)

        # retrieve compositor environment variables
        ret = Util.receiveFromUnixDomainSocket(FmConst.getRetrieveCompositorEnvironmentVariablesSocketFile(session_id))
        self._envDict = json.loads(ret.decode("utf-8"))

    def dispose(self):
        del self._envDict
        self._proc.terminate()
        self._proc.wait()
        del self._proc

    @property
    def envDict(self):
        return self._envDict


class PipeWire:

    def __init__(self, session_id):
        self._pwProc = None
        self._pwPulseProc = None
        try:
            self._pwProc = subprocess.Popen(["pipewire"])
            self._pwPulseProc = subprocess.Popen(["pipewire-pulse"])
        except BaseException:
            self.dispose()
            raise

    def dispose(self):
        if self._pwPulseProc is not None:
            self._pwPulseProc.terminate()
            self._pwPulseProc.wait()
            self._pwPulseProc = None
        if self._pwProc is not None:
            self._pwProc.terminate()
            self._pwProc.wait()
            self._pwProc = None


class Children:

    class DiedOp(enum.Enum):
        TERMINATE = "terminate"
        RESTART = "restart"
        NO_OP = "no-op"

    def __init__(self):
        self._callbacks = {}                                    # PID -> Function
        signal.signal(signal.SIGCHLD, self._childDiedSignalCb)

    def has_child(self, pid):
        return pid in self._callbacks

    def set_child_died_no_op(self, pid):
        self._callbacks[pid] = self._childDiedCallbackNoOp

    def set_child_died_autorestart(self, pid, command):
        self._callbacks[pid] = functools.partial(self._childDiedCallbackAutoRestart, pid, command)

    def set_child_died_callback(self, pid, custom_died_callback):
        self._callbacks[pid] = custom_died_callback

    def _childDiedSignalCb(self, sig, frame):
        logging.info("Got child_died signal... scheduling callback")
        GLib.idle_add(self._childDiedIdleCb)

    def _childDiedIdleCb(self):
        # This is called from the mainloop when idle, after one or more child
        # processes have died. The reason for waiting is that there is a window
        # between spawning a child and assigning its PID to a variable, during which
        # the signal handler may get called if the child exits very quickly.
        logging.info("child_died_callback")
        while True:
            try:
                pid, status = os.waitpid(-1, os.WNOHANG)
                if pid == 0:
                    logging.info("No zombie child processes")
                    return
            except OSError:
                logging.info("No more processes to reap")
                return
            logging.info("Child %s died with exit status %s", pid, status)
            fn = self._callbacks.get(pid, None)
            if fn is not None:
                del self._callbacks[pid]
                logging.info("Calling handler %s", fn)
                fn(status)

    def _childDiedCallbackAutoRestart(self, oldPid, command):
        newPid = os.spawnvp(os.P_NOWAIT, command[0], command)
        del self._callbacks[oldPid]
        self._callbacks[newPid] = functools.partial(self._childDiedCallbackAutoRestart, newPid, command)

    def _childDiedCallbackNoOp(self):
        pass


# def start(self):
#     self._pid = os.spawnvp(os.P_NOWAIT, self._command[0], self._command)
#     self._p._children.set_child_died_autorestart(self._pid, self._command)

# def stop(self):
#     if self._p._children.has_child(self._pid):
#         os.kill(self._pid, signal.SIGTERM)
